package namer

import (
	"path/filepath"
	"strings"

	"go-common/app/tool/gengo/types"
)

const (
	// GoSeperator is used to split go import paths.
	// Forward slash is used instead of filepath.Seperator because it is the
	// only universally-accepted path delimiter and the only delimiter not
	// potentially forbidden by Go compilers. (In particular gc does not allow
	// the use of backslashes in import paths.)
	// See https://golang.org/ref/spec#Import_declarations.
	// See also https://github.com/kubernetes/gengo/issues/83#issuecomment-367040772.
	GoSeperator = "/"
)

// IsPrivateGoName returns whether a name is a private Go name.
func IsPrivateGoName(name string) bool {
	return len(name) == 0 || strings.ToLower(name[:1]) == name[:1]
}

// NewPublicNamer is a helper function that returns a namer that makes
// CamelCase names. See the NameStrategy struct for an explanation of the
// arguments to this constructor.
func NewPublicNamer(prependPackageNames int, ignoreWords ...string) *NameStrategy {
	n := &NameStrategy{
		Join:                Joiner(IC, IC),
		IgnoreWords:         map[string]bool{},
		PrependPackageNames: prependPackageNames,
	}
	for _, w := range ignoreWords {
		n.IgnoreWords[w] = true
	}
	return n
}

// NewPrivateNamer is a helper function that returns a namer that makes
// camelCase names. See the NameStrategy struct for an explanation of the
// arguments to this constructor.
func NewPrivateNamer(prependPackageNames int, ignoreWords ...string) *NameStrategy {
	n := &NameStrategy{
		Join:                Joiner(IL, IC),
		IgnoreWords:         map[string]bool{},
		PrependPackageNames: prependPackageNames,
	}
	for _, w := range ignoreWords {
		n.IgnoreWords[w] = true
	}
	return n
}

// NewRawNamer will return a Namer that makes a name by which you would
// directly refer to a type, optionally keeping track of the import paths
// necessary to reference the names it provides. Tracker may be nil.
// The 'pkg' is the full package name, in which the Namer is used - all
// types from that package will be referenced by just type name without
// referencing the package.
//
// For example, if the type is map[string]int, a raw namer will literally
// return "map[string]int".
//
// Or if the type, in package foo, is "type Bar struct { ... }", then the raw
// namer will return "foo.Bar" as the name of the type, and if 'tracker' was
// not nil, will record that package foo needs to be imported.
func NewRawNamer(pkg string, tracker ImportTracker) *rawNamer {
	return &rawNamer{pkg: pkg, tracker: tracker}
}

// Names is a map from Type to name, as defined by some Namer.
type Names map[*types.Type]string

// Namer takes a type, and assigns a name.
//
// The purpose of this complexity is so that you can assign coherent
// side-by-side systems of names for the types. For example, you might want a
// public interface, a private implementation struct, and also to reference
// literally the type name.
//
// Note that it is safe to call your own Name() function recursively to find
// the names of keys, elements, etc. This is because anonymous types can't have
// cycles in their names, and named types don't require the sort of recursion
// that would be problematic.
type Namer interface {
	Name(*types.Type) string
}

// NameSystems is a map of a system name to a namer for that system.
type NameSystems map[string]Namer

// NameStrategy is a general Namer. The easiest way to use it is to copy the
// Public/PrivateNamer variables, and modify the members you wish to change.
//
// The Name method produces a name for the given type, of the forms:
// Anonymous types: <Prefix><Type description><Suffix>
// Named types: <Prefix><Optional Prepended Package name(s)><Original name><Suffix>
//
// In all cases, every part of the name is run through the capitalization
// functions.
//
// The IgnoreWords map can be set if you have directory names that are
// semantically meaningless for naming purposes, e.g. "proto".
//
// Prefix and Suffix can be used to disambiguate parallel systems of type
// names. For example, if you want to generate an interface and an
// implementation, you might want to suffix one with "Interface" and the other
// with "Implementation". Another common use-- if you want to generate private
// types, and one of your source types could be "string", you can't use the
// default lowercase private namer. You'll have to add a suffix or prefix.
type NameStrategy struct {
	Prefix, Suffix string
	Join           func(pre string, parts []string, post string) string

	// Add non-meaningful package directory names here (e.g. "proto") and
	// they will be ignored.
	IgnoreWords map[string]bool

	// If > 0, prepend exactly that many package directory names (or as
	// many as there are).  Package names listed in "IgnoreWords" will be
	// ignored.
	//
	// For example, if Ignore words lists "proto" and type Foo is in
	// pkg/server/frobbing/proto, then a value of 1 will give a type name
	// of FrobbingFoo, 2 gives ServerFrobbingFoo, etc.
	PrependPackageNames int

	// A cache of names thus far assigned by this namer.
	Names
}

// IC ensures the first character is uppercase.
func IC(in string) string {
	if in == "" {
		return in
	}
	return strings.ToUpper(in[:1]) + in[1:]
}

// IL ensures the first character is lowercase.
func IL(in string) string {
	if in == "" {
		return in
	}
	return strings.ToLower(in[:1]) + in[1:]
}

// Joiner lets you specify functions that preprocess the various components of
// a name before joining them. You can construct e.g. camelCase or CamelCase or
// any other way of joining words. (See the IC and IL convenience functions.)
func Joiner(first, others func(string) string) func(pre string, in []string, post string) string {
	return func(pre string, in []string, post string) string {
		tmp := []string{others(pre)}
		for i := range in {
			tmp = append(tmp, others(in[i]))
		}
		tmp = append(tmp, others(post))
		return first(strings.Join(tmp, ""))
	}
}

func (ns *NameStrategy) removePrefixAndSuffix(s string) string {
	// The join function may have changed capitalization.
	lowerIn := strings.ToLower(s)
	lowerP := strings.ToLower(ns.Prefix)
	lowerS := strings.ToLower(ns.Suffix)
	b, e := 0, len(s)
	if strings.HasPrefix(lowerIn, lowerP) {
		b = len(ns.Prefix)
	}
	if strings.HasSuffix(lowerIn, lowerS) {
		e -= len(ns.Suffix)
	}
	return s[b:e]
}

var (
	importPathNameSanitizer = strings.NewReplacer("-", "_", ".", "")
)

// filters out unwanted directory names and sanitizes remaining names.
func (ns *NameStrategy) filterDirs(path string) []string {
	allDirs := strings.Split(path, GoSeperator)
	dirs := make([]string, 0, len(allDirs))
	for _, p := range allDirs {
		if ns.IgnoreWords == nil || !ns.IgnoreWords[p] {
			dirs = append(dirs, importPathNameSanitizer.Replace(p))
		}
	}
	return dirs
}

// Name See the comment on NameStrategy.
func (ns *NameStrategy) Name(t *types.Type) string {
	if ns.Names == nil {
		ns.Names = Names{}
	}
	if s, ok := ns.Names[t]; ok {
		return s
	}

	if t.Name.Package != "" {
		dirs := append(ns.filterDirs(t.Name.Package), t.Name.Name)
		i := ns.PrependPackageNames + 1
		dn := len(dirs)
		if i > dn {
			i = dn
		}
		name := ns.Join(ns.Prefix, dirs[dn-i:], ns.Suffix)
		ns.Names[t] = name
		return name
	}

	// Only anonymous types remain.
	var name string
	switch t.Kind {
	case types.Builtin:
		name = ns.Join(ns.Prefix, []string{t.Name.Name}, ns.Suffix)
	case types.Map:
		name = ns.Join(ns.Prefix, []string{
			"Map",
			ns.removePrefixAndSuffix(ns.Name(t.Key)),
			"To",
			ns.removePrefixAndSuffix(ns.Name(t.Elem)),
		}, ns.Suffix)
	case types.Slice:
		name = ns.Join(ns.Prefix, []string{
			"Slice",
			ns.removePrefixAndSuffix(ns.Name(t.Elem)),
		}, ns.Suffix)
	case types.Pointer:
		name = ns.Join(ns.Prefix, []string{
			"Pointer",
			ns.removePrefixAndSuffix(ns.Name(t.Elem)),
		}, ns.Suffix)
	case types.Struct:
		names := []string{"Struct"}
		for _, m := range t.Members {
			names = append(names, ns.removePrefixAndSuffix(ns.Name(m.Type)))
		}
		name = ns.Join(ns.Prefix, names, ns.Suffix)
	case types.Chan:
		name = ns.Join(ns.Prefix, []string{
			"Chan",
			ns.removePrefixAndSuffix(ns.Name(t.Elem)),
		}, ns.Suffix)
	case types.Interface:
		// TODO: add to name test
		names := []string{"Interface"}
		for _, m := range t.Methods {
			// TODO: include function signature
			names = append(names, m.Name.Name)
		}
		name = ns.Join(ns.Prefix, names, ns.Suffix)
	case types.Func:
		// TODO: add to name test
		parts := []string{"Func"}
		for _, pt := range t.Signature.Parameters {
			parts = append(parts, ns.removePrefixAndSuffix(ns.Name(pt)))
		}
		parts = append(parts, "Returns")
		for _, rt := range t.Signature.Results {
			parts = append(parts, ns.removePrefixAndSuffix(ns.Name(rt)))
		}
		name = ns.Join(ns.Prefix, parts, ns.Suffix)
	default:
		name = "unnameable_" + string(t.Kind)
	}
	ns.Names[t] = name
	return name
}

// ImportTracker allows a raw namer to keep track of the packages needed for
// import. You can implement yourself or use the one in the generation package.
type ImportTracker interface {
	AddType(*types.Type)
	LocalNameOf(packagePath string) string
	PathOf(localName string) (string, bool)
	ImportLines() []string
}

type rawNamer struct {
	pkg     string
	tracker ImportTracker
	Names
}

// Name makes a name the way you'd write it to literally refer to type t,
// making ordinary assumptions about how you've imported t's package (or using
// r.tracker to specifically track the package imports).
func (r *rawNamer) Name(t *types.Type) string {
	if r.Names == nil {
		r.Names = Names{}
	}
	if name, ok := r.Names[t]; ok {
		return name
	}
	if t.Name.Package != "" {
		var name string
		if r.tracker != nil {
			r.tracker.AddType(t)
			if t.Name.Package == r.pkg {
				name = t.Name.Name
			} else {
				name = r.tracker.LocalNameOf(t.Name.Package) + "." + t.Name.Name
			}
		} else {
			if t.Name.Package == r.pkg {
				name = t.Name.Name
			} else {
				name = filepath.Base(t.Name.Package) + "." + t.Name.Name
			}
		}
		r.Names[t] = name
		return name
	}
	var name string
	switch t.Kind {
	case types.Builtin:
		name = t.Name.Name
	case types.Map:
		name = "map[" + r.Name(t.Key) + "]" + r.Name(t.Elem)
	case types.Slice:
		name = "[]" + r.Name(t.Elem)
	case types.Pointer:
		name = "*" + r.Name(t.Elem)
	case types.Struct:
		elems := []string{}
		for _, m := range t.Members {
			elems = append(elems, m.Name+" "+r.Name(m.Type))
		}
		name = "struct{" + strings.Join(elems, "; ") + "}"
	case types.Chan:
		// TODO: include directionality
		name = "chan " + r.Name(t.Elem)
	case types.Interface:
		// TODO: add to name test
		elems := []string{}
		for _, m := range t.Methods {
			// TODO: include function signature
			elems = append(elems, m.Name.Name)
		}
		name = "interface{" + strings.Join(elems, "; ") + "}"
	case types.Func:
		// TODO: add to name test
		params := []string{}
		for _, pt := range t.Signature.Parameters {
			params = append(params, r.Name(pt))
		}
		results := []string{}
		for _, rt := range t.Signature.Results {
			results = append(results, r.Name(rt))
		}
		name = "func(" + strings.Join(params, ",") + ")"
		if len(results) == 1 {
			name += " " + results[0]
		} else if len(results) > 1 {
			name += " (" + strings.Join(results, ",") + ")"
		}
	default:
		name = "unnameable_" + string(t.Kind)
	}
	r.Names[t] = name
	return name
}
